# *****************************************************************************
# *
# * Copyright (C) 2020-2024 Advanced Micro Devices, Inc. All rights reserved.
# *
# ******************************************************************************

import sys
import re
import os
import TestFileStrings as tc_strings
import argparse
from pathlib import Path
import glob
import textwrap

constants_file_name = "AmlTestConstants.h"
main_test_file_name = "AmlTestMain.c"
inf_file_name = "AmlUnitTests.inf"
host_inf_file_name = "AmlHostUnitTests.inf"
header_file_name = "AmlTests.h"

# MAX_LINE_LEN defines the maximum string length for multi line
# buffer constants.
MAX_LINE_LEN=79

class TestCase:
    """
        Defines a Test Case object that represents an ASL based test case

        Properties:
            test_name    - Camel case name of the test case. This is used as
                           the test function name.
            test_comment - First asl line of this test case.  Used to
                           match and populate consant files with aml data.
            test_results_name - C style name of the data buffer for this
                                test case.
            test_buffer  - Data buffer of hex constants for this test case.
    """

    def __init__(self, test_name, test_comment):
        """
            Creates a Test Case based on the test name and test comment
            specified in the input asl test file.

            Args:
                test_name    - Camel case name of the test case.
                test_comment - First asl line of this test case.
        """
        self.test_name = test_name
        self.test_comment = test_comment
        self.test_results_name = "Asl" + self.test_name
        self.test_buffer = ""
        print (self.test_comment)

    def generate_test_function(self):
        """
            Returns a string with the test function skeleton code for this
            particular test case.
        """
        return tc_strings.test_def.format(
                              test_comment = self.test_comment,
                              test_name = self.test_name,
                              test_results_name = self.test_results_name)

    def generate_test_call(self, test_suite_name):
        """
            Returns the string used to call the test case function

            Args:
                test_suite_name - Name of the test suite that this test
                                  case is a part of.
        """
        return tc_strings.add_test.format(
                              test_comment = self.test_comment.replace("\"", "\\\""),
                              suite_name = test_suite_name,
                              test_name = self.test_name)

    def find_my_const(self, buffer_file):
        """
            Parse the input constants source file to find and populate hex
            data for each test case

            Args:
                buffer_file - iasl produced output file containing C styled
                              hex data buffers for each of the test cases.
        """
        self.comment_found = False
        with open (buffer_file) as b_file:
            for b_line in b_file:
                # Only look for hex after finding test case in .c file
                if self.comment_found:
                    hex_match = re.search(r"(0x[0-9a-fA-F][0-9a-fA-F],)+", b_line)
                    if hex_match:
                        self.test_buffer += hex_match.group()
                        print (hex_match.group())
                    else:
                        complete_match = re.search(r"\s+};", b_line) or re.search(r"\A\n", b_line, re.MULTILINE)
                        if complete_match:
                            return
                else:
                    if self.test_comment in b_line:
                        if 'test_comment' not in b_line:
                            print("Found %s!!" % self.test_comment)
                            self.comment_found = True

    def format_test_buffer(self):
        """
            Removes trailing comma from string, used for parsing hex data
            iasl output. Add space after other commas to allow for multi-line
            formatting.
        """
        self.test_buffer = self.test_buffer.rstrip(',')
        self.test_buffer = re.sub(r"(?<=[,])(?=[^\s])", r" ", self.test_buffer)



class TestSuiteFile:
    """
        Test Suite File Object - Creates, parses, and builds a unit test file for
        a particular ASL test file

        Properties:
            test_asl_file   - Name of the ASL test file containing the test
                              suite data and test cases.
            test_file_name  - Name of the C Unit Test file generated by this
                              script.
            suite_name      - Generic name of the test suite specified at the
                              beginning of the ASL test file.
            caps_name       - Generic name of the test suite in all caps
            aml_file        - Name of the AML file generated by call to iasl.
            aml_const_file  - Name of the test constants file where the iasl
                              output is stored after being parsed.
            test_cases      - List of test cases specified by this test suite.
    """

    def __init__(self, asl_file):
        """
            Parses the ASL test file and saves test suite properties based on
            values found.

            Args:
                asl_file - Name of the file containing test suite data and test cases
        """
        self.test_asl_file = asl_file
        with open (self.test_asl_file, 'r') as asl_file:
            for line in asl_file:

                # search for the C test suite destination file name
                file_search =  re.search(r"test_file_name\s+(\w+.c)\s+", line)
                if None != file_search:
                    self.test_file_name = "{}".format(file_search.group(1))

                # search for the Test Suite name
                file_search =  re.search(r"suite_name\s+(\w+)\s+", line)
                if None != file_search:
                    self.suite_name = file_search.group(1)
                    self.caps_name = self.suite_name.upper()

                # search for AML file name
                file_search =  re.search(r"\"(\w+).aml\"", line)
                if None != file_search:
                    self.aml_file = "{}.aml".format(file_search.group(1))
                    self.aml_const_file = "{}.c".format(file_search.group(1))

                self.test_cases = list()


    def gather_test_cases(self):
        """
            Parses the ASL test file to collect the test case data for each
            case specified and creates a list of test cases.
        """
        with open (self.test_asl_file, 'r') as asl_file:
            contents = asl_file.read()

            # find all of the test cases in the file
            file_search =  re.findall(r"test_name\s*(\w+)\s+//\s+test_comment\s*([\w\"\'\.\(\)\_\\\,\{\} ]*)$", contents, re.DOTALL | re.MULTILINE)
            print(file_search)
            if None != file_search:
                for case in file_search:
                    print(case)
                    name, comment = case
                    self.test_cases.append(TestCase(name, comment))
            else:
                sys.exit("Unable to find any tests in file")

    def create_test_suite_file(self):
        """
            Creates and populates the C Test Suite file with test suite and
            test case data.
        """
        # Check if file already exists
        if(os.path.exists("..//{}".format(self.test_file_name))):
            sys.exit("Test file already exists.")

        with open ("..//{}".format(self.test_file_name), 'w') as out_file:

            # Add Test Suite Header
            header_string = tc_strings.amd_header.format(
                                            caps_name = self.caps_name,
                                            suite_name = self.suite_name)
            out_file.write(header_string)

            # Add each test case with buffer data to file
            for case in self.test_cases:
                case.find_my_const(self.aml_const_file)

                case.format_test_buffer()
                test_case_string = tc_strings.test_case_result_start.format(
                    test_results_name = case.test_results_name)

                # Limit Line Size to 79 characters
                for sized_line in textwrap.wrap(case.test_buffer, MAX_LINE_LEN):
                    test_case_string += tc_strings.test_case_result_data.format(
                        test_hex = sized_line)

                test_case_string += tc_strings.test_case_result_end

                out_file.write(test_case_string)

            # Add Input Validation Test Cases
            input_val_string = tc_strings.amd_input_validation.format(
                suite_name = self.suite_name)
            out_file.write(input_val_string)

            # Add Skeleton Code for Test Cases
            for case in self.test_cases:
                out_file.write(case.generate_test_function())

            # Write Test Suite Main Function
            main_func_header = tc_strings.test_main_start.format(
                                   suite_name = self.suite_name,
                                   caps_name = self.caps_name)
            out_file.write(main_func_header)

            # Add Test Cases to Test Suite Main
            for case in self.test_cases:
                out_file.write(case.generate_test_call(self.suite_name))

            # Write Test Suite Ending
            out_file.write(tc_strings.test_main_close)

    def create_test_constants(self):
        """
            Creates and populates an AML constants file with test case AML
            data as it corresponds to the test cases.  The resulting
            constants file is a copy of the existing constants file with
            the new test cases and values inserted.
        """
        constants_file = find_file(constants_file_name)
        const_file = open(constants_file, 'r')
        contents = const_file.readlines()
        const_file.close()

        # Find // INSERT HERE
        insert_index = contents.index("// INSERT HERE\n")
        insert_index +=1
        # Insert test suite header after the INSERT HERE string
        contents.insert(insert_index, tc_strings.suite_const_header.format(
                        suite_name = self.suite_name))
        insert_index +=1

        # Add each test case and data to the constants file
        for case in self.test_cases:
            # Search for duplicate variable in file
            for line in contents:
                if case.test_results_name in line:
                    sys.exit("Found duplicate variable: {}".format(case.test_results_name))

            contents.insert(insert_index, tc_strings.test_case_extern.format(
                test_results_name = case.test_results_name))
            insert_index +=1

        # Write and close constants file
        const_file = open(constants_file, 'w')
        const_file.writelines(contents)
        const_file.close()

    def add_call_to_main(self):
        """
            Adds the main function of this Test Suite to the set of functions
            called by the Main Unit Test c file along with the associated
            error checking.
        """

        main_test_file = find_file(main_test_file_name)
        main_file = open(main_test_file, 'r')
        contents = main_file.readlines()
        main_file.close()

        # Find // INSERT HERE
        insert_index = contents.index("// INSERT HERE\n")
        insert_index +=1

        # Check if call already exists in AmlTestMain
        for line in contents:
            if self.suite_name in line:
                sys.exit("Found existing call to {} in AmlTestMain".format(self.suite_name))

        # Insert call to main test suite
        contents.insert(insert_index, tc_strings.test_call_main.format(suite_name = self.suite_name))

        main_file = open(main_test_file, 'w')
        main_file.writelines(contents)
        main_file.close()

    def add_to_inf(self, inf_name):
        """
            Adds the Test Suite c file created by this script to the set of
            Unit Test files included in the Unit Test inf file.

            Args:
                inf_name - Name of INF file we are adding this test suite to.
        """
        inf_test_file = find_file(inf_name)
        inf_file = open(inf_test_file, 'r')
        contents = inf_file.readlines()
        inf_file.close()

        # Find // INSERT HERE
        insert_index = contents.index("# INSERT HERE\n")
        insert_index +=1

        # Check if file already exists in inf
        for line in contents:
            if self.test_file_name in line:
                sys.exit("Found existing file {} in inf".format(self.test_file_name))

        # Insert test suite c file to inf
        contents.insert(insert_index, tc_strings.test_file.format(
                        test_file_name = self.test_file_name))

        inf_file= open(inf_test_file, 'w')
        inf_file.writelines(contents)
        inf_file.close()

    def add_to_header(self):
        """
            Adds the main function header for the Test Suite to the Unit Test
            header file.
        """

        header_file = find_file(header_file_name)
        h_file = open(header_file, 'r')
        contents = h_file.readlines()
        h_file.close()

        # Find // INSERT HERE
        insert_index = contents.index("// INSERT HERE\n")
        insert_index +=1

        # Check if function already exists in header file
        for line in contents:
            if self.suite_name in line:
                sys.exit("Found existing function {} in header".format(self.suite_name))

        # Insert test suite main call to header
        contents.insert(insert_index, tc_strings.main_test_header.format(
                        suite_name = self.suite_name))

        h_file = open(header_file, 'w')
        h_file.writelines(contents)
        h_file.close()

    def clean_up_files(self):
        """
            Cleans up the files generated by running this script.
        """
        if os.path.exists(self.aml_file):
            os.remove(self.aml_file)
        else:
            print("Aml file does not exist")

        if os.path.exists(self.aml_const_file):
            os.remove(self.aml_const_file)
        else:
            print("C AML file does not exist")


def parse_input_params():
    """
        Parses input arguments for values needed by function
    """
    parser = argparse.ArgumentParser()
    parser.add_argument("asl_file", help="ASL test suite source file")
    return parser.parse_args()

def find_file(file_name):
    """
        Starts at current directory (UnitTest\Asl\ directory) and walks
        the file path backwards in order to locate the file indicated.
        Returns a string with the the absolute file path to the file.

            Args:
                file_name - Name of the file the script is trying to locate.
    """
    current_path = Path.cwd()
    root_dir = Path.cwd().anchor
    print(root_dir)

    file_path_list = sorted(current_path.rglob(file_name))
    while len(file_path_list) == 0:
        if current_path == root_dir:
            sys.exit("Unable to find file in {}".format(root_dir))
        current_path = current_path.parent
        file_path_list = sorted(current_path.rglob(file_name))

    return str(file_path_list[0])

def main():
    """
        Main function for this file.
    """
    # Parse input parameters here
    args = parse_input_params()

    iasl_exe = find_file('iasl.exe')

    # Create the Test Suite File object - this is where we collect asl file
    # information that will be used to build the <name>Test.c file
    # as well as fill out the test cases in the AmlTestConstants.h file.
    suite = TestSuiteFile(args.asl_file)

    # Gather test cases
    suite.gather_test_cases()

    # Generate AML file
    os.system("{} -sc {}".format(iasl_exe, suite.test_asl_file))

    # Write Test Suite to file
    suite.create_test_suite_file()

    # Write Test Constants
    suite.create_test_constants()

    # Add Test Call to Main Test Function
    suite.add_call_to_main()

    # Add Test to Header File
    suite.add_to_header()

    # Add File to Target Based inf
    suite.add_to_inf(inf_file_name)

    # Add File to Host Based inf
    suite.add_to_inf(host_inf_file_name)

    # Clean up files
    suite.clean_up_files()

#----------------------------------------------------------------------
#      Main
#
#  python TestFileGen.py <ASL File>
#
#----------------------------------------------------------------------
if __name__ == "__main__":
    # Call main function
    main()
